/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2020-2021. All rights reserved.
 * secPaver is licensed under the Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *     http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
 * PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

/*
Package builder implements the selinux policy builder.
*/
package builder

import (
	"bytes"
	"fmt"
	pb "gitee.com/openeuler/secpaver/api/proto/project"
	"gitee.com/openeuler/secpaver/common/ack"
	"gitee.com/openeuler/secpaver/common/global"
	"gitee.com/openeuler/secpaver/common/log"
	"gitee.com/openeuler/secpaver/common/project"
	"gitee.com/openeuler/secpaver/common/utils"
	"gitee.com/openeuler/secpaver/engine/selinux/pkg/secontext"
	"gitee.com/openeuler/secpaver/engine/selinux/pkg/sehandle"
	"gitee.com/openeuler/secpaver/engine/selinux/pkg/semodule"
	"gitee.com/openeuler/secpaver/engine/selinux/pkg/sepolicy"
	"gitee.com/openeuler/secpaver/engine/selinux/pkg/serule"
	"gitee.com/openeuler/secpaver/engine/selinux/proto"
	"github.com/pkg/errors"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const (
	fileAttribute   = "file_type"
	domainAttribute = "domain"
)

// Builder is the selinux policy builder
type Builder struct {
	conf       *proto.Config // the config for selinux engine
	seHandle   *sehandle.Handle
	fileSeInfo map[string]*fileSeInfo
}

// NewBuilder is the constructor of Builder
func NewBuilder() *Builder {
	return &Builder{
		fileSeInfo: map[string]*fileSeInfo{},
	}
}

// Build generate selinux policy modules with project build information
func (b *Builder) Build(prjData *pb.ProjectData, out string, msg chan *pb.Ack) error {
	var err error
	name := prjData.GetMeta().GetName()

	// 1. parse selinux engine config
	b.conf, err = parseConfig(prjData)
	if err != nil {
		return errors.Wrap(err, "fail to parse selinux engine config")
	}

	// 2. expand all macros and groups in project files
	if err := project.ProcessMacroAndGroup(prjData); err != nil {
		return err
	}

	// 3. create system selinux handle
	b.seHandle, err = sehandle.HandleCreate()
	if err != nil {
		return errors.Wrap(err, "fail to create system policy handle")
	}

	// 4. set the mask file context and type define
	cilInfo, err := semodule.GetModuleCilInfo(name)
	if err == nil {
		for _, tp := range cilInfo.ProvideTypes {
			b.seHandle.RemoveMaskType(tp)
		}

		for _, path := range cilInfo.FileConPaths {
			b.seHandle.RemoveMaskFileContext(path)
		}
	}

	// 5. process file context
	if err = b.processFileContext(prjData); err != nil {
		return err
	}

	// 6. process application context
	if err = b.processApplicationContext(prjData); err != nil {
		return err
	}

	// 7. create policy
	policy, err := sepolicy.CreateSePolicy(prjData.GetMeta().GetName(), "1.0")
	if err != nil {
		return err
	}

	// 8. add all file context and domain definition to policy
	for _, info := range b.fileSeInfo {
		if info.fileContext != nil && info.fileContext.IsLocal {
			policy.AddFileContext(info.fileContext)
			tp := info.fileContext.Context.Type
			if !b.seHandle.TypeExist(tp) {
				policy.AddTypeAttrDefine(tp, b.conf.FileAttr)
			}
		}

		domain := info.execDomain
		if domain != "" && !b.seHandle.TypeExist(domain) {
			policy.AddTypeAttrDefine(domain, b.conf.DomainAttr)
		}
	}

	// 9. add extra rules to policy
	for _, rule := range b.conf.GetExtraRules() {
		r, err := serule.ParseRule(rule)
		if err != nil {
			return errors.Wrap(err, "fail to parse extra rules")
		}

		if err := policy.AddRulesWithHandle(b.seHandle, r); err != nil {
			return errors.Wrap(err, "fail to add extra rules to policy")
		}
	}

	// 10. add application rules to policy
	if err := b.addAppRulesToPolicy(policy, prjData, msg); err != nil {
		return err
	}

	// 11. deal rules conflict and export policy to files
	// dealAndReportTypeConflict(policy, msg)
	if err := b.generatePolicyFiles(policy, filepath.Join(out, policy.Name)); err != nil {
		return errors.Wrapf(err, "fail to generate %s policy files", policy.Name)
	}

	return nil
}

func (b *Builder) addAppRulesToPolicy(policy *sepolicy.Policy, prjData *pb.ProjectData, msg chan *pb.Ack) error {
	var allRules []serule.Rule
	gener := ruleGenerator{
		handle:   b.seHandle,
		fileInfo: b.fileSeInfo,
	}

	for _, appInfo := range prjData.GetApplicationList().GetApplicationList() {
		app := appInfo.GetApplication()
		path := app.GetPath()
		fileInfo, ok := b.fileSeInfo[path]
		if !ok || fileInfo == nil || fileInfo.fileContext == nil {
			return fmt.Errorf("fail to get file context of application %s", path)
		}

		subType := fileInfo.fileContext.Context.Type

		// 1. add domain transition rules
		transRules, _ := serule.CreateDomainAutoTransRule(
			b.conf.GetDomainAttr(), subType, fileInfo.execDomain)
		allRules = append(allRules, transRules...)

		// 2. add permissive flag for domain
		if app.GetIsPermissive() {
			policy.AddPermissiveDomain(subType)
		}

		// 3. add permission rules
		for _, permission := range appInfo.GetPermissionList() {
			rules, err := gener.generateRules(subType, permission, msg)
			if err != nil {
				return errors.Wrapf(err, "fail to gen rules for application %s", path)
			}

			allRules = append(allRules, rules...)
		}
	}

	return policy.AddRulesWithHandle(b.seHandle, allRules...)
}

func (b *Builder) generatePolicyFiles(policy *sepolicy.Policy, outDir string) error {
	if err := os.RemoveAll(outDir); err != nil {
		log.Errorf("fail to remove policy output directory %s", outDir)
		return fmt.Errorf("fail to remove policy output directory")
	}

	if err := compilePolicy(policy, outDir); err != nil {
		return errors.Wrap(err, "fail to compile policy")
	}

	if err := writeResourceList(filepath.Join(outDir, "resourcelist"), b.fileSeInfo); err != nil {
		return errors.Wrap(err, "fail to create resourcelist file")
	}

	var types []string
	for tp := range policy.Defines.TypeAttrDefine {
		types = append(types, tp)
	}

	if err := writeScripts(policy.Name, types, outDir); err != nil {
		return errors.Wrap(err, "fail to create policy script files")
	}

	return nil
}

func compilePolicy(policy *sepolicy.Policy, out string) error {
	teFile := filepath.Join(out, policy.Name+".te")
	fcFile := filepath.Join(out, policy.Name+".fc")

	if err := utils.WriteFile(teFile, []byte(policy.TeText()), global.DefaultFilePerm); err != nil {
		return errors.Wrapf(err, "fail to write %s file", filepath.Base(teFile))
	}

	if err := utils.WriteFile(fcFile, []byte(policy.FcText()), global.DefaultFilePerm); err != nil {
		return errors.Wrapf(err, "fail to write %s file", filepath.Base(fcFile))
	}

	modFile := strings.TrimSuffix(teFile, ".te") + ".mod"

	cmd := exec.Command("checkmodule", "-mMo", modFile, teFile)
	cmdOut, err := cmd.CombinedOutput()
	if err != nil {
		log.Errorf("fail to run checkmodule command: %s", string(cmdOut))
		return fmt.Errorf("fail to run checkmodule command")
	}

	ppFile := strings.TrimSuffix(teFile, ".te") + ".pp"

	if fcFile == "" {
		cmd = exec.Command("semodule_package", "-o", ppFile, "-m", modFile)
	} else {
		cmd = exec.Command("semodule_package", "-o", ppFile, "-m", modFile, "-f", fcFile)
	}

	cmdOut, err = cmd.CombinedOutput()
	if err != nil {
		log.Errorf("fail to run semodule_package command: %s", string(cmdOut))
		return fmt.Errorf("fail to run semodule_package command")
	}

	return nil
}

func writeResourceList(path string, resources map[string]*fileSeInfo) error {
	var paths []string
	for path, res := range resources {
		paths = append(paths, getResourceListPaths(path, res.class == secontext.DirFile)...)
	}

	paths = utils.RemoveRepeatedElement(paths)

	var data bytes.Buffer
	for _, path := range paths {
		if _, err := data.WriteString(path + "\n"); err != nil {
			return err
		}
	}

	if err := utils.WriteFile(path, data.Bytes(), global.DefaultFilePerm); err != nil {
		return fmt.Errorf("fail to write file")
	}

	return nil
}

func dealAndReportTypeConflict(p *sepolicy.Policy, msg chan *pb.Ack) {
	if m := p.DealTypeConflict(); len(m) != 0 {
		for t1, t2 := range m {
			sendMsg(msg, ack.LevelWarn, fmt.Sprintf(
				"type %s will be replaced by %s due to type_transition rule conflict", t1, t2))
		}
	}
}

func sendMsg(ch chan *pb.Ack, level, msg string) {
	if ch == nil {
		return
	}

	ch <- &pb.Ack{
		Level:  level,
		Status: msg,
	}
}

func parseConfig(project *pb.ProjectData) (*proto.Config, error) {
	conf := &proto.Config{ // default setting
		DomainAttr: domainAttribute,
		FileAttr:   fileAttribute,
	}

	confMap := project.GetEngineConf()
	if confMap != nil {
		rawConf, ok := confMap["selinux"]
		if ok && rawConf != nil {
			if err := utils.UnmarshalPbStruct(rawConf, conf); err != nil {
				return nil, err
			}
		}
	}

	log.Debugf("selinux engine config: %+v", conf)

	return conf, nil
}

type ruleGenerator struct {
	handle   *sehandle.Handle
	fileInfo map[string]*fileSeInfo
}

func (g *ruleGenerator) generateRules(subType string, perm *pb.Application_Permission, msg chan *pb.Ack) ([]serule.Rule, error) {
	switch perm.GetType() {
	case project.RuleFileSystem:
		return g.generateFileRules(subType, perm.GetResources(), perm.GetActions())

	case project.RuleCapability:
		return g.generateCapabilityRules(subType, perm.GetActions())

	case project.RuleNetWork:
		return g.generateNetRules(subType, perm.GetResources(), perm.GetActions())

	default:
		sendMsg(msg, ack.LevelWarn, fmt.Sprintf("invalid permission type %s", perm.Type))
		return nil, nil
	}
}
